跳到主要内容

第二章:创建第一个 Python MCP Server

上一章,我们已经明确了 Health Tracker 的开发思路:

ChatGPT 负责理解用户需求,MCP Server 负责提供工具,SQLite 负责保存数据。

这一章,我们暂时不开发健康记录功能,也不连接数据库,而是从零开始创建一个最简单的 MCP Server。

我们的目标只有三个:让 MCP Server 成功启动、让 MCP Inspector 发现工具、让工具能够被正常调用。

2.1 创建 Python 项目​

首先打开终端,创建项目目录:

mkdir health-tracker-agent
cd health-tracker-agent

本课程使用 uv 管理 Python 环境和依赖。

如果已经安装了 uv,可以直接初始化项目:

uv init

初始化后,目录中会出现类似下面的文件:

health-tracker-agent/
├── .python-version
├── README.md
├── main.py
└── pyproject.toml

其中,我们需要重点关注两个文件。

main.py 是 Python 程序的入口文件,接下来要在这里编写 MCP Server。

pyproject.toml 则负责保存项目的基本信息和依赖配置。

安装 MCP SDK​

执行:

uv add "mcp>=2,<3"

这里使用 MCP Python SDK 2.x,因此后面的代码都按照 SDK 2.x 的接口编写。

安装完成后,项目中还会生成 uv.lock 文件,用于锁定依赖版本。

现在,我们已经有了一个可以开发 MCP Server 的 Python 项目。

2.2 编写第一个 MCP Server​

打开 main.py,删除初始化时生成的示例代码,替换成下面的内容。

from mcp.server.mcpserver import MCPServer

mcp = MCPServer("Health Tracker")


@mcp.tool()
def hello(name: str) -> str:
"""根据用户姓名返回一段问候语。"""
return f"你好,{name}!这是我的第一个 MCP 工具。"


if __name__ == "__main__":
mcp.run(
transport="streamable-http",
host="127.0.0.1",
port=8000,
)

先不急着理解每一行代码,我们直接运行它。

uv run python main.py

如果服务正常启动,终端会保持运行状态,等待客户端连接。

我们的 MCP Server 地址是:

http://127.0.0.1:8000/mcp

注意:这是 MCP 协议接口,并不是一个普通网页。

直接在浏览器地址栏中打开它,不一定会看到我们想象中的页面,因为 MCP 客户端需要按照协议发送请求。

那么,应该如何测试它呢?

这就需要使用 MCP Inspector。

2.3 使用 MCP Inspector 测试工具​

MCP Inspector 是专门用于调试 MCP Server 的工具。

它可以帮助我们查看服务提供了哪些工具、每个工具接收什么参数,以及调用工具后返回什么结果。

保持 Python MCP Server 正常运行,再打开一个终端。

执行:

npx @modelcontextprotocol/inspector

第一次运行时,npx 可能会提示安装相关依赖,按照终端提示操作即可。

启动后,终端会显示 Inspector 的本地访问地址。打开该地址,进入调试界面。

连接 MCP Server​

在 Inspector 中,选择:

  • Transport Type:Streamable HTTP
  • URL:http://127.0.0.1:8000/mcp

然后点击连接。

如果连接成功,就可以继续查看 MCP Server 提供的工具。

查看工具列表​

找到 Tools 页面,点击列出工具。

正常情况下,我们应该看到:

hello

工具的描述为:

根据用户姓名返回一段问候语。

同时,Inspector 还可以识别这个工具需要一个参数:

name: string

这就说明,我们刚才编写的 Python 函数,已经成功注册成了 MCP 工具。

调用 hello 工具​

接下来,选择 hello,输入参数:

{
"name": "Andy"
}

点击执行。

如果一切正常,返回结果应包含:

你好,Andy!这是我的第一个 MCP 工具。

到这里,我们就完成了第一个 MCP Server 的开发与测试。

不过,有一个问题值得思考:

Inspector 为什么知道我们编写了一个叫 hello 的工具?它又是怎么知道这个工具需要一个字符串参数的?

接下来,我们通过代码解释其中的原理。

2.4 MCP Server 是如何注册工具的?​

重新回到 main.py。

首先看这一行:

mcp = MCPServer("Health Tracker")

它创建了一个 MCP Server 实例,并将服务器名称设置为 Health Tracker。

这里的名称是服务器标识,不是 Python 文件名,也不是工具名称。

接下来是:

@mcp.tool()
def hello(name: str) -> str:
"""根据用户姓名返回一段问候语。"""
return f"你好,{name}!这是我的第一个 MCP 工具。"

其中最关键的是:

@mcp.tool()

这是一个 Python 装饰器。

它的作用是把下面的函数注册为 MCP 工具。

如果没有这个装饰器,hello() 就只是一个普通 Python 函数,MCP 客户端不会自动把它当作可调用工具。

工具名称从哪里来?​

def hello(name: str) -> str:

函数名 hello 就是默认的工具名称。

所以,Inspector 才会在工具列表中显示 hello。

工具参数从哪里来?​

注意函数中的类型注解:

name: str

它告诉 MCP SDK,name 参数应该是字符串。

MCP SDK 可以根据函数签名和类型注解生成工具的输入 Schema。

因此,客户端能够知道工具需要哪些参数以及参数类型。

工具描述从哪里来?​

函数中的文档字符串:

"""根据用户姓名返回一段问候语。"""

不仅是写给程序员看的注释,也可以作为工具描述提供给 MCP 客户端。

这非常重要。

因为当我们将 MCP Server 连接到 ChatGPT 后,ChatGPT 需要理解每个工具的用途,才能判断什么时候应该调用它。

所以,在 MCP 开发中,函数名称、参数类型和 Docstring 都应该尽量准确。

2.5 MCP Server 是如何接收请求的?​

接下来,看程序最后的代码:

if __name__ == "__main__":
mcp.run(
transport="streamable-http",
host="127.0.0.1",
port=8000,
)

这里涉及三个重要概念。

transport:通信方式​

transport="streamable-http"

我们选择的是 Streamable HTTP 传输方式。

它允许 MCP 客户端通过 HTTP 与 MCP Server 通信。

本课程后面需要通过 Tunnel 将本地 MCP Server 连接到 ChatGPT,因此使用 Streamable HTTP 比较合适。

MCP 还支持其他传输方式,例如 stdio,它通常用于客户端直接启动本地 MCP Server 进程,并通过标准输入输出通信。

host:监听地址​

host="127.0.0.1"

127.0.0.1 表示本机回环地址。

这意味着当前服务主要供本机程序访问。

我们在开发阶段使用这个地址,可以避免将服务直接暴露给局域网中的其他设备。

后续部署到 Docker 容器时,我们会根据容器网络环境调整监听地址。

port:监听端口​

port=8000

这里指定 MCP Server 使用 8000 端口。

如果端口已经被其他程序占用,可以更换为其他未使用的端口,但 Inspector 中的连接地址也需要同步修改。

2.6 为什么 Inspector 能够自动发现工具?​

现在,我们已经知道 @mcp.tool() 可以注册 Python 函数。

但是 Inspector 并不是通过读取 main.py 文件发现这些工具的。

它根本不需要访问我们的 Python 源代码。

真正的过程是:

image-20261021093837759

当 Inspector 与 MCP Server 建立连接后,双方会进行初始化交互。

随后,Inspector 可以发送 tools/list 请求,获取服务器提供的工具列表。

例如,服务器可能返回类似下面的工具描述:

{
"name": "hello",
"description": "根据用户姓名返回一段问候语。",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": ["name"]
}
}

这里展示的是简化后的结构,用来帮助理解工具 Schema;实际返回内容可能包含其他字段。

当我们在 Inspector 中输入 Andy 并点击执行时,客户端又会发送 tools/call 请求。

请求中包含工具名称以及参数:

{
"name": "hello",
"arguments": {
"name": "Andy"
}
}

MCP Server 根据工具名称找到对应的 Python 函数,完成调用,再将结果返回。

因此,MCP 的关键并不是让客户端直接执行 Python 源代码,而是通过统一协议发现和调用服务端提供的工具。

2.7 添加第二个 MCP 工具​

为了进一步理解工具注册机制,我们再增加一个简单的计算工具。

在 main.py 中,继续添加:

@mcp.tool()
def add(a: float, b: float) -> float:
"""计算两个数字的和。"""
return a + b

现在,完整代码如下:

from mcp.server.mcpserver import MCPServer

mcp = MCPServer("Health Tracker")


@mcp.tool()
def hello(name: str) -> str:
"""根据用户姓名返回一段问候语。"""
return f"你好,{name}!这是我的第一个 MCP 工具。"


@mcp.tool()
def add(a: float, b: float) -> float:
"""计算两个数字的和。"""
return a + b


if __name__ == "__main__":
mcp.run(
transport="streamable-http",
host="127.0.0.1",
port=8000,
)

保存文件后,停止之前运行的 MCP Server,然后重新启动:

uv run python main.py

回到 MCP Inspector,重新连接或刷新工具列表。

现在应该能够看到两个工具:

hello
add

选择 add,输入:

{
"a": 10,
"b": 20
}

执行后,应返回计算结果:

30.0

这个例子说明,一个 MCP Server 可以注册多个工具。

未来的 Health Tracker 就是利用同样的方式,注册 record_food、record_weight、get_daily_summary 等工具。

2.8 常见问题​

问题一:启动时报端口占用​

如果出现类似:

Address already in use

说明 8000 端口已经被其他程序占用。

有两种解决办法:关闭占用端口的程序,或者给 MCP Server 换一个端口。

1.Mac:查找并关闭占用程序​

在终端运行:

lsof -nP -iTCP:8000 -sTCP:LISTEN

可能看到:

COMMAND   PID   USER   FD   TYPE  NAME
Python 12345 andy 10u IPv4 TCP *:8000 (LISTEN)

其中:

  • Python:占用端口的程序。
  • 12345:进程编号,也就是 PID。

先确认这是不是之前启动的服务。 如果它所在的终端还在,回到那个终端按 Ctrl + C 即可停止。

如果找不到原来的终端,可以执行:

kill 12345

把 12345 替换成你查到的 PID。如果普通关闭无效,再强制结束:

kill -9 12345

然后重新启动 MCP Server。

2.Windows:查找并关闭占用程序​

打开 命令提示符 CMD,运行:

netstat -ano | findstr :8000

找到本地地址端口为 8000,且状态为 LISTENING 的一行:

TCP    0.0.0.0:8000    0.0.0.0:0    LISTENING    12345

最后的 12345 就是进程 PID。查看它对应什么程序:

tasklist /FI "PID eq 12345"

如果是之前启动的 MCP Server,优先回到原来的终端按 Ctrl + C 停止。

也可以根据 PID 结束进程:

taskkill /PID 12345 /F

如果提示“拒绝访问”,以管理员身份打开 CMD,再执行。

3.如果占用程序还要继续使用,就换端口​

例如,把 MCP Server 从 8000 改为 8001。

如果你使用 Python MCP SDK 的 FastMCP,并在创建实例时配置端口,可以修改为:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP(
"Health Tracker",
host="0.0.0.0",
port=8001,
)

if __name__ == "__main__":
mcp.run(transport="streamable-http")

如果你通过 Uvicorn 启动,则修改启动命令中的端口:

uvicorn main:app --host 0.0.0.0 --port 8001

这两种方式按项目实际启动方式选择即可。

换端口后,相关连接也要同步修改。 例如,Tunnel 原来转发到 http://localhost:8000,现在就要改成 http://localhost:8001;直接连接本地服务的客户端,也要更新地址。

端口就像窗口号,8000 号窗口已经有人使用,我们可以让原来的程序退出,也可以换到 8001 号窗口。

问题二:Inspector 无法连接 MCP Server​

首先确认 Python 服务仍然运行。

然后检查 Inspector 中是否选择了正确的传输方式:

Streamable HTTP

以及正确的 URL:

http://127.0.0.1:8000/mcp

特别注意最后的 /mcp 路径。

问题三:新增工具后,Inspector 看不到​

通常是因为修改 Python 代码后,没有重新启动 MCP Server,或者 Inspector 仍在使用之前的连接状态。

可以先重启 Python 服务,再重新连接 Inspector,并刷新工具列表。

问题四:MCP 工具能否直接访问数据库?​

可以。

MCP 工具本质上就是 Python 函数。

只要服务端程序拥有相应权限,函数就可以执行 SQLite 查询、访问文件或调用其他服务。

不过,MCP 工具的权限应遵循最小权限原则,尤其是后面接入 ChatGPT、开放网络访问时,不能将没有身份验证的敏感数据接口直接暴露到公网。

2.9 本章总结​

这一章,我们从一个空目录开始,完成了第一个 MCP Server 的开发。

回顾一下整个过程:

  1. 使用 uv 初始化 Python 项目。
  2. 安装 MCP Python SDK 2.x。
  3. 创建 MCPServer 实例。
  4. 使用 @mcp.tool() 注册 Python 函数。
  5. 通过 Streamable HTTP 启动服务。
  6. 使用 MCP Inspector 发现并调用工具。
  7. 增加第二个工具,验证一个 MCP Server 可以提供多种能力。

其中,最重要的知识点是:

MCP Server 将 Python 函数以标准化工具接口的形式提供给 MCP 客户端。

客户端不需要了解 Python 源代码,只需要通过 MCP 协议获取工具的名称、描述和参数 Schema,就能够调用这些工具。

到这里,我们已经具备了开发 Health Tracker 的基础。

下一章预告:使用 SQLite 保存饮食记录​

目前,我们编写的 hello 和 add 工具只能返回计算结果,还没有保存任何数据。

下一章,我们将开始开发第一个真正有实际用途的健康记录工具:

record_food(...)

我们会先创建 SQLite 数据库和饮食记录表,再编写数据保存与查询功能。

最终要实现的效果是:通过 MCP Inspector 调用 record_food,将一条饮食记录真正写入 SQLite,并且在程序重启后依然能够查询到它。

从下一章开始,Health Tracker 将不再只是一个演示程序,而是逐步成为能够保存真实数据的应用。

课程推荐

想系统学习这门课程?

你已阅读完本文。需要更完整的知识体系,可以继续了解「Vibe Coding 实战课程」。

查看课程详情